import React, {Component} from 'react'
import {MarkdownPreview} from 'react-marked-markdown';
// import py1_1 from '../static/py1_1.md'
// import marked from 'marked';

class Py15 extends Component {
    constructor(props) {
        super(props);
        this.state = {
           value: `

虽然在命令行里显示运行结果很有意思，但是随着数据不断增多，并且需要进行数据分析
时，将数据打印到命令行就不是办法了。为了可以远程使用大部分网络爬虫，你还需要把
采集到的数据存储起来。

本章将介绍三种主要的数据管理方法，对绝大多数应用都适用。如果你准备创建一个网站
的后端服务或者创建自己的 API，那么可能都需要让爬虫把数据写入数据库。如果你需要
一个快速简单的方法收集网上的文档，然后存到你的硬盘里，那么可能需要创建一个文件
流（file stream）来实现。如果还要为偶然事件提个醒儿，或者每天定时收集当天累计的数
据，就给自己发一封邮件吧！

抛开与网络数据采集的关系，大数据存储和与数据交互的能力，在新式的程序开发中也已
经是重中之重了。这一章的内容其实是实现第二部分许多示例的基础。如果你对自动数据
存储相关的知识不太了解，我非常希望你至少能浏览一下。

> 5.1　媒体文件

存储媒体文件有两种主要的方式：只获取文件 URL 链接，或者直接把源文件下载下来。
你可以通过媒体文件所在的 URL 链接直接引用它。这样做的优点如下。
* 爬虫运行得更快，耗费的流量更少，因为只要链接，不需要下载文件。
* 可以节省很多存储空间，因为只需要存储 URL 链接就可以。
* 存储 URL 的代码更容易写，也不需要实现文件下载代码。
* 不下载文件能够降低目标主机服务器的负载。

不过这么做也有一些缺点。
* 这些内嵌在你的网站或应用中的外站 URL 链接被称为盗链（hotlinking），使用盗链可
能会让你麻烦不断，每个网站都会实施防盗链措施。
* 因为你的链接文件在别人的服务器上，所以你的应用就要跟着别人的节奏运行了。
* 盗链是很容易改变的。如果你把盗链图片放在博客上，要是被对方服务器发现，很可能
被恶搞。如果你把 URL 链接存起来准备以后再用，可能用的时候链接已经失效了，或
者是变成了完全无关的内容。
* 现实中的网络浏览器不仅可以请求 HTML 页面并切换页面，它们也会下载访问页面上
所有的资源。下载文件会让你的爬虫看起来更像是人在浏览网站，这样做反而有好处。

如果你还在犹豫究竟是存储文件，还是只存储文件的 URL 链接，可以想想这些文件是要
多次使用，还是放进数据库之后就只是等着“落灰”，再也不会被打开。如果答案是后者，
那么最好还是只存储这些文件的 URL 吧。如果答案是前者，那么就继续往下看！

在 Python 3.x 版本中， urllib.request.urlretrieve 可以根据文件的 URL 下载文件：
  
    from urllib.request import urlretrieve
    from urllib.request import urlopen
    from bs4 import BeautifulSoup

    html = urlopen("http://www.pythonscraping.com")
    bsObj = BeautifulSoup(html)
    imageLocation = bsObj.find("a", {"id": "logo"}).find("img")["src"]
    urlretrieve (imageLocation, "logo.jpg")
     
这段程序从\http://pythonscraping.com下载 logo 图片，然后在程序运行的文件夹里保存为
logo.jpg 文件。

如果你只需要下载一个文件，而且知道如何获取它，以及它的文件类型，这么做就可以
了。但是大多数爬虫都不可能一天只下载一个文件。下面的程序会把 \http://pythonscraping.
com 主页上所有 src 属性的文件都下载下来：
 
    import os
    from urllib.request import urlretrieve
    from urllib.request import urlopen
    from bs4 import BeautifulSoup

    downloadDirectory = "downloaded"
    baseUrl = "http://pythonscraping.com"

    def getAbsoluteURL(baseUrl, source):
        if source.startswith("http://www."):
            url = "http://"+source[11:]
        elif source.startswith("http://"):
            url = source
        elif source.startswith("www."):
            url = source[4:]
            url = "http://"+source
        else:
            l = baseUrl+"/"+source
        if baseUrl not in url:
            return None
        return url
    def getDownloadPath(baseUrl, absoluteUrl, downloadDirectory):
        path = absoluteUrl.replace("www.", "")
        path = path.replace(baseUrl, "")
        path = downloadDirectory+path
        directory = os.path.dirname(path)

        if not os.path.exists(directory):
            os.makedirs(directory)

        return path
    html = urlopen("http://www.pythonscraping.com")
    bsObj = BeautifulSoup(html)
    downloadList = bsObj.findAll(src=True)

    for download in downloadList:
        fileUrl = getAbsoluteURL(baseUrl, download["src"])
        if fileUrl is not None:
            print(fileUrl)
    urlretrieve(fileUrl, getDownloadPath(baseUrl, fileUrl, downloadDirectory))

#### 程序运行注意事项

你知道从网上下载未知文件的那些警告吗？这个程序会把页面上所有的文件
下载到你的硬盘里，可能会包含一些 bash 脚本、.exe 文件，甚至可能是恶意
软件（malware）。

如果你之前从没有运行过任何下载到电脑里的文件，电脑就是安全的吗？尤
其是当你用管理员权限运行这个程序时，你的电脑基本已经处于危险之中。
如果你执行了网页上的一个文件，那个文件把自己传送到了 ../../../../usr/bin/
python 里面，会发生什么呢？等下一次你再运行 Python 程序时，你的电脑
就可能会安装恶意软件。

这个程序只是为了演示；请不要随意运行它，因为这里没有对所有下载文件
的类型进行检查，也不应该用管理员权限运行它。记得经常备份重要的文
件，不要在硬盘上存储敏感信息，小心驶得万年船。

这个程序首先使用 Lambda 函数（第 2 章介绍过）选择首页上所有带 src 属性的标签。然
后对 URL 链接进行清理和标准化，获得文件的绝对路径（而且去掉了外链）。最后，每个
文件都会下载到程序所在文件夹的 downloaded 文件里。

这里 Python 的 os 模块用来获取每个下载文件的目标文件夹，建立完整的路径。os 模块是
Python 与操作系统进行交互的接口，它可以操作文件路径，创建目录，获取运行进程和环
境变量的信息，以及其他系统相关的操作。

> 5.2　把数据存储到CSV

CSV（Comma-Separated Values，逗号分隔值）是存储表格数据的常用文件格式。Microsoft
Excel 和很多应用都支持 CSV 格式，因为它很简洁。下面就是一个 CSV 文件的例子：
   
    fruit,cost
    apple,1.00
    banana,0.30
    pear,1.25
   
和 Python 一样，CSV 里留白（whitespace）也是很重要的：每一行都用一个换行符分隔，
列与列之间用逗号分隔（因此也叫“逗号分隔值”）。CSV 文件还可以用 Tab 字符或其他字
符分隔行，但是不太常见，用得不多。

如果你只想从网页上把 CSV 文件下载到电脑里，不打算做任何解析和修改，那么这节后
面的内容就没必要再看了。只要用上一节里介绍的文件下载方法下载并保存为 CSV 格式
就行了。

Python 的 csv 库可以非常简单地修改 CSV 文件，甚至从零开始创建一个 CSV 文件：
   
    import csv

    csvFile = open("../files/test.csv", 'w+')
    try:
        writer = csv.writer(csvFile)
        writer.writerow(('number', 'number plus 2', 'number times 2'))
        for i in range(10):
            writer.writerow( (i, i+2, i*2))
    finally:
        csvFile.close()
 
这里提个醒儿：Python 新建文件的机制考虑得非常周到（bullet-proof）。如果 ../files/test.csv
不存在，Python 会自动创建文件（不会自动创建文件夹）。如果文件已经存在，Python 会
用新的数据覆盖 test.csv 文件。

运行完成后，你会看到一个 CSV 文件：
   
    number,number plus 2,number times 2
    0,2,0
    1,3,2
    2,4,4
    ...

网络数据采集的一个常用功能就是获取 HTML 表格并写入 CSV 文件。维基百科的文本编
辑器对比词条（https://en.wikipedia.org/wiki/Comparison_of_text_editors）中用了许多复杂
的 HTML 表格，用到了颜色、链接、排序，以及其他在写入 CSV 文件之前需要忽略的
HTML 元素。用 BeautifulSoup 和 get_text() 函数，你可以用十几行代码完成这件事：
    
    import csv
    from urllib.request import urlopen
    from bs4 import BeautifulSoup

    html = urlopen("http://en.wikipedia.org/wiki/Comparison_of_text_editors")
    bsObj = BeautifulSoup(html)
    # 主对比表格是当前页面上的第一个表格
    table = bsObj.findAll("table",{"class":"wikitable"})[0]
    rows = table.findAll("tr")

    csvFile = open("../files/editors.csv", 'wt', newline=", encoding='utf-8')
    writer = csv.writer(csvFile)
    try:
        for row in rows:
        csvRow = []
        for cell in row.findAll(['td', 'th']):
            csvRow.append(cell.get_text())
            writer.writerow(csvRow)
    finally:
        csvFile.close()

<center>实际工作中写此程序之前的注意事项</center>
   
    如果你有很多 HTML 表格，且每个都要转换成 CSV 文件，或者许多 HTML 表格都要
    汇总到一个 CSV 文件，那么把这个程序整合到爬虫里以解决问题非常好。但是，如果
    你只需要做一次这种事情，那么更好的办法就是：复制粘贴。选择 HTML 表格内容然
    后粘贴到 Excel 文件里，可以另存为 CSV 格式，不需要写代码就能搞定！
  

这个程序会在程序上一层目录的 files 文件夹里生成一个 CSV 文件 ../files/editors.csv——把
这个程序分享给那些不熟悉 MySQL 的朋友们吧！

> 5.3　MySQL

MySQL（官方发音是“My es-kew-el”，但很多人都说成“My Sequel”）是目前最受欢迎
的开源关系型数据库管理系统。一个开源项目具有如此之竞争力实在是令人意外，它的流
行程度正在不断地接近另外两个闭源的商业数据库系统：微软的 SQL Server 和甲骨文的
Oracle 数据库（MySQL 在 2010 年被甲骨文收购）。
它的流程程度实在是名符其实。对大多数应用来说，MySQL 都是不二选择。它是一种
非常灵活、稳定、功能齐全的 DBMS，许多顶级的网站都在用它：YouTube 1 、Twitter 2 和
Facebook 3 等。

> 5.3.1　安装MySQL

如果你第一次接触 MySQL，安装数据库听着可能有点儿吓人（如果你是老手，可以跳过
这部分内容）。其实，安装方法和安装其他软件一样简单。归根到底，MySQL 就是由一系
列数据文件构成的，储存在你的远端服务器或本地的电脑上，里面包含了数据库存储的所
有信息。MySQL 软件层提供了一种与数据交互的便捷操作方法。例如，下面的命令把用
户表 users 中名字为“Ryan”的用户找出来：
   
    SELECT * FROM users WHERE firstname = "Ryan"
   
如果你用 Ubuntu（或其他 Debain 分支系统），安装 MySQL 很简单：
   
    $sudo apt-get install mysql-server
    
只要稍微留意一下安装过程，看看电脑是不是可以满足安装的内存需求，然后在安装提示
的地方为 root 用户设置新密码就可以了。

Mac OS X 和 Windows 系统的安装过程有点儿复杂。如果你没有甲骨文账户，下载 MySQL
安装包之前需要先注册一下。

> 5.3.2　基本命令

MySQL 服务器启动之后，有很多种方法可以与数据库交互。因为有很多工具是图形界面，
所以你不用 MySQL 的命令行（或者很少用命令行）也能管理数据库。像 phpMyAdmin 和
MySQL Workbench 这类工具都可以很容易地实现数据的查看、排序和新建等工作。但是，
掌握命令行操作数据库依然是很重要的。

除了用户自定义变量名（MySQL 5.x 版本是不区分大小写的，MySQL 5.0 之前的版本是不
区分大小写的），MySQL 语句是不区分大小写的。例如，SELECT 和 sElEcT 是一样的，不
过习惯上写 MySQL 语句的时候所有的 MySQL 关键词都用大写。大多数开发者还喜欢用
小写字母表示数据表和数据库的名称，虽然这个标准经常不被注意。

当你首次登入 MySQL 的时候，里面是没有数据库存放数据的。你可以创建一个：
   
    >CREATE DATABASE scraping;
   
因为每个 MySQL 实例可以有多个数据库，所以使用某个数据库之前需要指定数据库的
名称：
   
    >USE scraping;
   
从现在开始（直到关闭 MySQL 链接或切换到另一个数据库之前），所有的命令都运行在这
个新的“scraping”数据库里面。

所有操作看着都非常简单。那么，在数据库里创建数据表的方法应该也类似吧？让我们在
数据库里创建一个表来存储采集的网页：
   
    >CREATE TABLE pages;
   
结果显示错误：
  
    ERROR 1113 (42000): A table must have at least 1 column
 
和数据库不同，MySQL 数据表必须至少有一列，否则不能创建。为了在 MySQL 里定义字
段（数据列），你必须在 CREATE TABLE <tablename> 语句后面，把字段的定义放进一个带括
号的、内部由逗号分隔的列表中：
   
    >CREATE TABLE pages (id BIGINT(7) NOT NULL AUTO_INCREMENT, title VARCHAR(200),
    content VARCHAR(10000), created TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY
    (id));
   
每个字段定义由三部分组成：

* 名称（ id 、 title 、 created 等）
* 数据类型（ BIGINT(7) 、 VARCHAR 、 TIMESTAMP ）
* 其他可选属性（ NOT NULL AUTO_INCREMENT ）

在字段定义列表的最后，还要定义一个“主键”（key）。MySQL 用这个主键来组织表的内
容，便于后面快速查询。在本章后面的内容里，我将介绍如何调整这些主键以提高数据库
的查询速度，但是现在，用表的 id 列作为主键就可以。

语句执行之后，你可以用 DESCRIBE 查看数据表的结构

当然，这还是一个空表。你可以在 pages 表里插入一些测试数据，如下所示：
   
    > INSERT INTO pages (title, content) VALUES ("Test page title", "This is some test page content. It can be up to 10,000 characters long.");
   
需要注意的是，虽然 pages 表里有四个字段（ id 、 title 、 content 、 created ），但实际上
你只需要插入两个字段（ title 和 content ）的数据即可。因为 id 字段是自动递增的（每
次新插入数据行时 MySQL 自动增加 1），通常不用处理。另外， created 字段的类型是
timestamp ，默认就是数据加入时的时间戳。

当然，我们也可以自定义四个字段的内容：
 
    INSERT INTO pages (id, title, content, created) VALUES (3, "Test page title", "
    This is some test page content. It can be up to 10,000 characters long.", "2014-
    09-21 10:25:32");
 
只要你定义的整数在数据表的 id 字段里没有，它就可以顺利插入数据表。但是，这么做
非常不好；除非万不得已（比如程序中断漏了一行数据），否则让 MySQL 自己处理 id 和
timestamp 字段。

现在表里有一些数据了，你可以用很多方法来选择这些数据。下面是几个 SELECT 语句的
示例：
     
    >SELECT * FROM pages WHERE id = 2;
  
这条语句告诉 MySQL，“从 pages 表中把 id 字段中等于 2 的整行数据全挑选出来”。这个
星号（ * ）是通配符，表示所有字段，这行语句会把满足条件（ WHERE id = 2 ）的所有字段
都显示出来。如果 id 字段里没有任何一行等于 2，就会返回一个空集。例如，下面这个不
区分大小写的查询，会返回 title 字段里包含“test”的所有行（ % 符合表示 MySQL 字符
串通配符）的所有字段：
   
    >SELECT * FROM pages WHERE title LIKE "%test%";
   
但是，如果你的表有很多字段，而你只想返回部分字段怎么办？你可以不用星号，而用下
面的方式：
  
    >SELECT id, title FROM pages WHERE content LIKE "%page content%";
 
这样就只会返回 title 字段包含“page content”的所有行的 id 和 title 两个字段了。
DELETE 语句语法与 SELECT 语句类似：
    
    >DELETE FROM pages WHERE id = 1;
  
由于数据库的数据删除后不能恢复，所以在执行 DELETE 语句之前，建议用 SELECT 确认
一下要删除的数据（本例中，就是用 \SELECT * FROM pages WHERE id = 1 查看），然后把
SELECT * 换成 DELETE 就可以了，这会是一个好习惯。很多程序员都有过一些 DELETE 误操
作的伤心往事，还有一些恐怖故事就是有人慌乱中忘了在语句中放 WHERE ，结果把所有客
户数据都删除了。别让这种事发生在你身上！

还有一个需要介绍的语句是 UPDATE ：
    
    >UPDATE pages SET title="A new title", content="Some new content" WHERE id=2;
 
结合本书的主题，后面我们就只用这些基本的 MySQL 语句，做一些简单的数据查询、创
建和更新工作。如果你对这个强大数据库的命令和技术感兴趣，推荐你去看 Paul DuBois
的 MySQL Cookbook（\http://shop.oreilly.com/product/0636920032274.do ）。

> 5.3.3　与Python整合

Python 没有内置的 MySQL 支持工具。不过，有很多开源的库可以用来与 MySQL 做交互，
Python 2.x 和 Python 3.x 版本都支持。最有名的一个库就是 PyMySQL（https://github.com/
PyMySQL/PyMySQL）。

写到这里的时候，PyMySQL 的版本是 0.6.2，你可以用下面的命令下载并安装它：
   
    $ curl -L https://github.com/PyMySQL/PyMySQL/tarball/pymysql-0.6.2 | tar xz
    $ cd PyMySQL-PyMySQL-f953785/
    $ python setup.py install
     
如果需要更新，请检查最新版的 PyMySQL，并修改第一行下载链接中的版本号进行更新。
安装完成之后，你就可以使用 PyMySQL 包了。如果你的 MySQL 服务器处于运行状态，
应该就可以成功地执行下面的命令（记得把 root 账户密码加进去）：
   
    import pymysql
    conn = pymysql.connect(host='127.0.0.1', unix_socket='/tmp/mysql.sock',
                            user='root', passwd=None, db='mysql')
    cur = conn.cursor()
    cur.execute("USE scraping")

    cur.execute("SELECT * FROM pages WHERE id=1")
    print(cur.fetchone())
    cur.close()
    conn.close()
   
这段程序有两个对象：连接对象（ conn ）和光标对象（ cur ）。

连接 / 光标模式是数据库编程中常用的模式，不过刚刚接触数据库的时候，有些用户很难
区分两种模式的不同。连接模式除了要连接数据库之外，还要发送数据库信息，处理回滚
操作（当一个查询或一组查询被中断时，数据库需要回到初始状态，一般用事务控制手段
实现状态回滚），创建新的光标对象，等等。

而一个连接可以有很多个光标。一个光标跟踪一种状态（state）信息，比如跟踪数据库的
使用状态。如果你有多个数据库，且需要向所有数据库写内容，就需要多个光标来处理。
光标还会包含最后一次查询执行的结果。通过调用光标函数，比如 cur.fetchone() ，可以
获取查询结果。

用完光标和连接之后，千万记得把它们关闭。如果不关闭就会导致连接泄漏（connection
leak），造成一种未关闭连接现象，即连接已经不再使用，但是数据库却不能关闭，因为数
据库不能确定你还要不要继续使用它。这种现象会一直耗费数据库的资源，所以用完数据
库之后记得关闭连接！

刚开始的时候，你最想做的事情可能就是把采集的结果保存到数据库里。让我们用前面维
基百科爬虫的例子来演示一下如何实现数据存储。
在进行网络数据采集时，处理 Unicode 字符串是很痛苦的事情。默认情况下，MySQL 也
不支持 Unicode 字符处理。不过你可以设置这个功能（这么做会增加数据库的占用空间）。
因为在维基百科上我们难免会遇到各种各样的字符，所以最好一开始就让你的数据库支持
Unicode：
     
    ALTER DATABASE scraping CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
    ALTER TABLE pages CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
    ALTER TABLE pages CHANGE title title VARCHAR(200) CHARACTER SET utf8mb4 COLLATE
    utf8mb4_unicode_ci;
    ALTER TABLE pages CHANGE content content VARCHAR(10000) CHARACTER SET utf8mb4 CO
    LLATE utf8mb4_unicode_ci;
     
这四行语句改变的内容有：数据库、数据表，以及两个字段的默认编码都从 utf8mb4
（严格说来也属于 Unicode，但是对大多数 Unicode 字符的支持都非常不好）转变成了
utf8mb4_unicode_ci 。

你可以在 title 或 content 字段中插入一些德语变音符（umlauts）或汉语字符，如果没有
错误就表示转换成功了。

现在数据库已经准备好接收维基百科的各种信息了，你可以用下面的程序来存储数据：
    
    from urllib.request import urlopen
    from bs4 import BeautifulSoup
    import re
    import datetime
    import random
    import pymysql

    conn = pymysql.connect(host='127.0.0.1', unix_socket='/tmp/mysql.sock',
                            user='root', passwd=None, db='mysql', charset='utf8')
    cur = conn.cursor()
    cur.execute("USE scraping")

    random.seed(datetime.datetime.now())

    def store(title, content):
        cur.execute("INSERT INTO pages (title, content) VALUES (\"%s\",
                    \"%s\")", (title, content))
        cur.connection.commit()

    def getLinks(articleUrl):
        html = urlopen("http://en.wikipedia.org"+articleUrl)
        bsObj = BeautifulSoup(html)
        title = bsObj.find("h1").get_text()
        content = bsObj.find("div", {"id":"mw-content-text"}).find("p").get_text()
        store(title, content)
        return bsObj.find("div", {"id":"bodyContent"}).findAll("a",
                            href=re.compile("^(/wiki/)((?!:).)*$"))

    links = getLinks("/wiki/Kevin_Bacon")
    try:
        while len(links) > 0:
            newArticle = links[random.randint(0, len(links)-1)].attrs["href"]
            print(newArticle)
            links = getLinks(newArticle)
    finally:
        cur.close()
        conn.close()
   
这里有几点需要注意：首先， charset='utf8' 要增加到连接字符串里。这是让连接 conn 把
所有发送到数据库的信息都当成 UTF-8 编码格式（当然，前提是数据库默认编码已经设置
成 UTF-8）。

然后要注意的是 store 函数。它有两个参数： title 和 content ，并把这两个参数加到了一
个 INSERT 语句中并用光标执行，然后用光标进行连接确认。这是一个让光标与连接操作分
离的好例子；当光标里存储了一些数据库与数据库上下文（context）的信息时，需要通过
连接的确认操作先将信息传进数据库，再将信息插入数据库。

最后要注意的是， finally 语句是在程序主循环的外面，代码的最底下。这样做可以保证，
无论程序执行过程中如何发生中断或抛出异常（当然，因为网络很复杂，你得随时准备遭
遇异常），光标和连接都会在程序结束前立即关闭。无论你是在采集网络，还是处理一个
打开连接的数据库，用 try...finally 都是一个好主意。

虽然 PyMySQL 规模并不大，但是里面有一些非常实用的函数本书并没有介绍。具体请参
考 Python 的 DBAPI 标准文档（\http://legacy.python.org/dev/peps/pep-0249/）。

> 5.3.4　MySQL里的“六度空间游戏”

在第 3 章，我们介绍过“维基百科六度分隔”问题，其目标是通过一些词条链接寻找两个
词条间的联系（即找出一条链接路径，只要点击链接就可以从一个维基词条到另一个维基
词条）。

为了解决这个问题，我们不仅需要建立网络爬虫采集网页（之前我们已经做过），还要把
采集的信息以某种形式存储起来，以便后续进行数据分析。

前面介绍过的自增的 id 字段、时间戳以及多份数据表在这里都要用到。为了确定最合理
的信息存储方式，你需要先想想游戏规则。一个链接可以轻易地把页面 A 连接到页面 B。
同样也可以轻易地把页面 B 连接到页面 A，不过这可能是另一条链接。我们可以这样识
别一个链接，即“页面 A 存在一个链接，可以连接到页面 B”。也就是 INSERT INTO links
(fromPageId, toPageId) VALUES (A, B); （其中，“A”和“B”分别表示页面的 ID 号）。

因此需要设计一个带有两张数据表的数据库来分别存储页面和链接，两张表都带有创建时
间和独立的 ID 号，代码如下所示：
     
    CREATE TABLE 'wikipedia'.'pages' (
        'id' INT NOT NULL AUTO_INCREMENT,
        'url' VARCHAR(255) NOT NULL,
        'created' TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
        PRIMARY KEY ('id'));

    CREATE TABLE 'wikipedia'.'links' (
        'id' INT NOT NULL AUTO_INCREMENT,
        'fromPageId' INT NULL,
        'toPageId' INT NULL,
        'created' TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
        PRIMARY KEY ('id'));
   
注意，这里和前面打印页面标题的爬虫不同，我没有在页面数据表里使用页面标题字段，
为什么这么做呢？其实是因为页面标题要在你进入页面后读取内容才能抓到。那么，如果
我们想创建一个高效的爬虫来填充这些数据表，那么只存储页面的链接就可以保存词条页
面了，甚至不需要访问词条页面。

当然并不是所有网站都具有这个特点，但是维基百科的词条链接和对应的页面标题是可以
通过简单的操作进行转换的。例如，\http://en.wikipedia.org/wiki/Monty_Python 的后面就是
页面标题“Monty Python”。

下面的代码会把“贝肯数”（一个页面与凯文 ·贝肯词条页面的链接数）不超过 6 的维基
百科页面存储起来：
 
    from bs4 import BeautifulSoup
    import re
    import pymysql

    conn = pymysql.connect(host='127.0.0.1', unix_socket='/tmp/mysql.sock',user=
                        'root', passwd=None, db='mysql', charset='utf8')
    cur = conn.cursor()
    cur.execute("USE wikipedia")

    def insertPageIfNotExists(url):
        cur.execute("SELECT * FROM pages WHERE url = %s", (url))
        if cur.rowcount == 0:
            cur.execute("INSERT INTO pages (url) VALUES (%s)", (url))
            conn.commit()
            return cur.lastrowid
        else:
            return cur.fetchone()[0]

    def insertLink(fromPageId, toPageId):
        cur.execute("SELECT * FROM links WHERE fromPageId = %s AND toPageId = %s",
                        (int(fromPageId), int(toPageId)))
        if cur.rowcount == 0:
            cur.execute("INSERT INTO links (fromPageId, toPageId) VALUES (%s, %s)",
                                (int(fromPageId), int(toPageId)))
            conn.commit()

    pages = set()
    def getLinks(pageUrl, recursionLevel):
        global pages
        if recursionLevel > 4:
            return;
        pageId = insertPageIfNotExists(pageUrl)
        html = urlopen("http://en.wikipedia.org"+pageUrl)
        bsObj = BeautifulSoup(html)
        for link in bsObj.findAll("a",
                                    href=re.compile("^(/wiki/)((?!:).)*$")):
                                    insertLink(pageId,
                                        insertPageIfNotExists(link.attrs['href']))
        if link.attrs['href'] not in pages:
            # 遇到一个新页面，加入集合并搜索里面的词条链接
            newPage = link.attrs['href']
            pages.add(newPage)
            getLinks(newPage, recursionLevel+1)
    getLinks("/wiki/Kevin_Bacon", 0)
    cur.close()
    conn.close()
 
用递归实现那些需要运行很长时间的代码，通常是一件复杂的事情。在本例中，变量
recursionLevel 被传递到 getLinks 函数里，用来跟踪函数递归的次数（每完成一次递归，
recursionLevel 就加 1）。当 recursionLevel 值到 5 的时候，函数会自动返回，不会继续递
归。这个限制可以防止数据太大导致内存堆栈溢出。

需要注意的是，这个程序可能要运行好几天才会结束。虽然我自己运行过它，但是我的数
据库里只保持了一点点贝肯数不超过 6 的词条，因为维基百科服务器会拒绝程序的请求。
但是，这些数据对后面分析维基百科词条的链接路径问题已经足够了。

关于这个问题的补充和最终答案，将在第 8 章关于有向图的问题里介绍。

> 5.4　Email

与网页通过 HTTP 协议传输一样，邮件是通过 SMTP（Simple Mail Transfer Protocol，简单邮件传输协议）传输的。而且，和你用网络服务器的客户端（浏览器）处理那些通过
HTTP 协议传输的网页一样，Email 服务器也有客户端，像 Sendmail、Postfix 和 Mailman
等，都可以收发邮件。

虽然用 Python 发邮件很容易，但是需要你连接那些正在运行 SMTP 协议的服务器。在服
务器或本地机器上设置 SMTP 客户端有点儿复杂，也超出了本书的介绍范围，但是有很多
资料可以帮你解决问题，如果你用的是 Linux 或 Mac OS X 系统，参考资料会更丰富。

下面的代码运行的前提是你的电脑已经可以正常地运行一个 SMTP 客户端。（如果要调整
代码用于远程 SMTP 客户端，请把 localhost 改成远程服务器地址。）

用 Python 发一封邮件只要 9 行代码：
    
    import smtplib
    from email.mime.text import MIMEText

    msg = MIMEText("The body of the email is here")

    msg['Subject'] = "An Email Alert"
    msg['From'] = "ryan@pythonscraping.com"
    msg['To'] = "webmaster@pythonscraping.com"

    s = smtplib.SMTP('localhost')
    s.send_message(msg)
    s.quit()
 
Python 有两个包可以发送邮件： smtplib 和 email 。

Python 的 email 模块里包含了许多实用的邮件格式设置函数，可以用来创建邮件“包
裹”。下面的示例中使用的 MIMEText 对象，为底层的 MIME（Multipurpose Internet Mail
Extensions，多用途互联网邮件扩展类型）协议传输创建了一封空邮件，最后通过高层的
SMTP 协议发送出去。 MIMEText 对象 msg 包括收发邮箱地址、邮件正文和主题，Python 通
过它就可以创建一封格式正确的邮件。

smtplib 模块用来设置服务器连接的相关信息。就像 MySQL 服务器的连接一样，这个连接
必须在用完之后及时关闭，以避免同时创建太多连接而浪费资源。
把这个简单的邮件程序封装成函数后，可以更方便地扩展和使用：
   
    import smtplib
    from email.mime.text import MIMEText
    from bs4 import BeautifulSoup
    from urllib.request import urlopen
    import time

    def sendMail(subject, body):
        msg = MIMEText(body)
        msg['Subject'] = subject
        msg['From'] = "christmas_alerts@pythonscraping.com"
        msg['To'] = "ryan@pythonscraping.com"

    s = smtplib.SMTP('localhost')
    s.send_message(msg)
    s.quit()

    bsObj = BeautifulSoup(urlopen("https://isitchristmas.com/"))
    while(bsObj.find("a", {"id":"answer"}).attrs['title'] == "NO"):
        print("It is not Christmas yet.")
        time.sleep(3600)
    bsObj = BeautifulSoup(urlopen("https://isitchristmas.com/"))
    sendMail("It's Christmas!",
                        "According to http://itischristmas.com, it is Christmas!")
 
这个程序每小时检查一次 https://isitchristmas.com/ 网站（根据日期判断当天是不是圣诞
节）。如果页面上的信息不是“NO”（中国用户在网站页面上看到的“NO”在源代码里是\<noscript> 不是 \</noscript> ），就会给你发一封邮件，告诉你圣诞节到了。

虽然这个程序看起来并没有墙上的挂历有用，但是稍作修改就可以做很多有用的事情。它
可以发送网站访问失败、应用测试失败的异常情况，也可以在 Amazon 网站上出现了一款
卖到断货的畅销品时通知你——这些都是挂历做不到的事情。
    
           
           `,
        };
      }
    render() {
        return (
            <div >
                {/* 111111111111111
                <Py112 /> */}
                <MarkdownPreview value={this.state.value}/>
            </div>
        )
    }
}

export default Py15